﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Postnuclear.Dependency;
using Postnuclear.Test.Helpers;

namespace Postnuclear.Test.Dependency
  {
  /// <summary>
  /// Represents a test class for the <see cref="ExpressionBinding{TMember,TSource}"/> class.
  /// </summary>
  [TestClass]
  public sealed class ExpressionBindingTest
    : TestClassBase
    {
    #region Public

    // Methods

    /// <summary>
    /// A test for <see cref="ExpressionBinding{TMember, TSource}"/> constructor to check exception for target null expression.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(ArgumentNullException))]
    public void CreateWithNullTargetTest()
      {
      new ExpressionBinding<int, int>(null, () => 0);
      }

    /// <summary>
    /// A test for <see cref="ExpressionBinding{TMember, TSource}"/> constructor to check exception for source null expression.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(ArgumentNullException))]
    public void CreateWithNullSourceTest()
      {
      int result = 0;
      new ExpressionBinding<int, int>(() => result, null);
      }

    /// <summary>
    /// A test for main binding functionality.
    /// </summary>
    [TestMethod]
    public void BindingTest()
      {
      Mock<IValueStorage<int>> result = new Mock<IValueStorage<int>>();
      result.SetupProperty(m => m.Value, 0);

      Mock<IValueProvider<int>> expectedResult = new Mock<IValueProvider<int>>();
      expectedResult.SetAndRaise(result.Object.Value + 1);

      new ExpressionBinding<int, int>(() => result.Object.Value, () => expectedResult.Object.Value);
      Assert.AreEqual(expectedResult.Object.Value, result.Object.Value);

      expectedResult.SetAndRaise(result.Object.Value + 1);
      Assert.AreEqual(expectedResult.Object.Value, result.Object.Value);
      }

    /// <summary>
    /// A test for binding functionality when similar objects involved.
    /// </summary>
    [TestMethod]
    public void BindingTwoSimilarObjectsTest()
      {
      Mock<IValueStorage<int>> result = new Mock<IValueStorage<int>>();
      result.SetupProperty(m => m.Value, 0);

      Mock<IValueProvider<int>> first = new Mock<IValueProvider<int>>();
      first.SetAndRaise(1);

      Mock<IValueProvider<int>> second = new Mock<IValueProvider<int>>();
      second.SetAndRaise(1);

      new ExpressionBinding<int, int>(() => result.Object.Value, () => first.Object.Value + second.Object.Value);
      Assert.AreEqual(first.Object.Value + second.Object.Value, result.Object.Value);

      first.SetAndRaise(first.Object.Value + 1);
      Assert.AreEqual(first.Object.Value + second.Object.Value, result.Object.Value);

      second.SetAndRaise(second.Object.Value + 1);
      Assert.AreEqual(first.Object.Value + second.Object.Value, result.Object.Value);
      }

    /// <summary>
    /// A test for binding functionality when target instance is disposed.
    /// </summary>
    [TestMethod]
    public void DisposedTest()
      {
      Mock<IValueProvider<int>> source = new Mock<IValueProvider<int>>();
      source.SetAndRaise(1);

      Mock<IValueStorage<int>> target = new Mock<IValueStorage<int>>();
      Mock<IDisposed> disposed = target.As<IDisposed>();
      target.SetupProperty(m => m.Value);
      Assert.AreNotEqual(source.Object.Value, target.Object.Value);

      IValueStorage<int> targetObject = target.Object;
      new ExpressionBinding<int, int>(() => targetObject.Value, () => source.Object.Value);
      Assert.AreEqual(source.Object.Value, target.Object.Value);

      disposed.SetupGet(m => m.Disposed).Returns(true);
      source.SetAndRaise(2);
      Assert.AreNotEqual(source.Object.Value, target.Object.Value);
      }

    /// <summary>
    /// A test for binding functionality when expression includes method invocation.
    /// </summary>
    [TestMethod]
    public void MethodCallTest()
      {
      Mock<IValueStorage<int>> result = new Mock<IValueStorage<int>>();
      result.SetupProperty(m => m.Value, 0);

      Mock<IValueProvider<int>> source = new Mock<IValueProvider<int>>();
      source.SetAndRaise(1);
      Assert.AreNotEqual(source.Object.Value, result);

      new ExpressionBinding<int, int>(() => result.Object.Value, () => GetValueProvider(source.Object.Value).Value);
      Assert.AreEqual(source.Object.Value, result.Object.Value);

      source.SetAndRaise(2);
      Assert.AreEqual(source.Object.Value, result.Object.Value);
      }

    /// <summary>
    /// A test for binding functionality when static member included.
    /// </summary>
    [TestMethod]
    public void StaticMemberTest()
      {
      string expectedResult = "Expected";
      Mock<IValueStorage<string>> result = new Mock<IValueStorage<string>>();
      result.SetupProperty(m => m.Value, string.Empty);

      Mock<IValueProvider<bool>> predicate = new Mock<IValueProvider<bool>>();
      predicate.SetAndRaise(true);

      new ExpressionBinding<string, string>(() => result.Object.Value, () => predicate.Object.Value ? expectedResult : string.Empty);
      Assert.AreEqual(expectedResult, result.Object.Value);

      predicate.SetAndRaise(false);
      Assert.AreEqual(string.Empty, result.Object.Value);
      }

    /// <summary>
    /// A test for unbinding after source is garbage collected.
    /// </summary>
    [TestMethod]
    public void SourceGarbageCollectionTest()
      {
      Mock<IValueStorage<int>> result = new Mock<IValueStorage<int>>();
      result.SetupProperty(m => m.Value, 0);

      Mock<IValueProvider<int>> expectedResult = new Mock<IValueProvider<int>>();
      WeakReference<ExpressionBinding<int, int>> binding = new ExpressionBinding<int, int>(() => result.Object.Value, () => expectedResult.Object.Value);
      Assert.IsTrue(binding.IsAlive);

      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsTrue(binding.IsAlive);

      expectedResult = null;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsFalse(binding.IsAlive);
      }

    /// <summary>
    /// A test for unbinding after target is garbage collected (in case of multiple sources).
    /// </summary>
    [TestMethod]
    public void TargetGarbageCollectionTest()
      {
      IValueStorage<int> target = new Mock<IValueStorage<int>>().Object;
      Mock<IValueProvider<int>> source = new Mock<IValueProvider<int>>();
      WeakReference<ExpressionBinding<int, int>> binding = new ExpressionBinding<int, int>(() => target.Value, () => source.Object.Value);
      Assert.IsTrue(binding.IsAlive);

      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsTrue(binding.IsAlive);

      target = null;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsTrue(binding.IsAlive);

      source.RaiseValueChanged();
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsFalse(binding.IsAlive);
      }

    #endregion

    #region Method

    private static IValueProvider<TValue> GetValueProvider<TValue>(TValue value)
      {
      Mock<IValueProvider<TValue>> provider = new Mock<IValueProvider<TValue>>();
      provider.SetAndRaise(value);
      return provider.Object;
      }

    #endregion
    }
  }